/*
 * Copyright (c) 2010 Vadim Zhukov <persgray@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */


#include "common.h"

#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <arpa/nameser.h>

#include <err.h>
#include <errno.h>
#include <netdb.h>
#include <resolv.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "resolver.h"


#define PORT_CHARS	"0123456789"
#define IPv4_ADDR_CHARS	".0123456789"
/* XXX: Link addresses are "a:d:d:r%ifname". Should we keep supporting them? */
#define IPv6_ADDR_CHARS	":%0123456789" \
	"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

enum {
	SASPEC_RES_ERROR = -1,	/* Illegal specification or flags restriction */
	SASPEC_RES_PORT =   0,	/* No host name defined, but port found */
	SASPEC_RES_IPV4 =   1,
	SASPEC_RES_IPV6 =   2,
	SASPEC_RES_DN =     3,
	SASPEC_RES_LOCAL =  4,
};

static int	initialized = 0;

static int	split_addr_spec(const char *, char **, char **, int);
static int	build_sa(struct bindaddr *, const char*, const char*, int, int);

/*
 * Extracts host and, possibly, port part from given string.
 *
 * Return apporiate SASPEC_* constants, see resolver.h.
 *
 * NOTE: We do not check IPv6 and domain names throughly because it's a mess and
 * rules are changed every year. Anyway, we just bother about surely illegal
 * chars mostly.
 */
int
split_addr_spec(const char *spec, char **host, char **port, int flags) {
	size_t	 len, plen;
	int	 i, oerrno, rv;
	char	*h, *p;
	const	 char *e, *s;

	DPRINTFX(3, "called split_addr_spec(\"%s\", ..., %u)", spec, flags);

	p = h = NULL;
	len = strlen(spec);
	rv = SASPEC_RES_ERROR;
	if (len == 0)
		goto inval;
	if (spec[0] == '/') {
		/* local socket */
		if (flags & SASPEC_HOST_NETWORK)
			goto inval;
		h = strdup(spec);
		if (h == NULL)
			goto err;
		rv = SASPEC_RES_LOCAL;
		goto success;
	}
	if (strchr(spec, ':') != strrchr(spec, ':')) {
		/* IPv6 address */
		if (spec[0] == '[') {
			/* "[a:d:d:r]:port" or "[a:d:d:r]" */
			if (strchr(spec + 1, '[') != NULL)
				goto inval;
			e = strchr(spec + 1, ']');
			if (e == NULL || strchr(e + 1, ']') != e)
				goto inval;
			if (e[1] != '\0') {
				/* "[addr]:port" */
				if (e[1] != ':' || e[2] == '\0')
					goto inval;
				plen = strlen(e) - 2;
				if (strspn(e + 2, PORT_CHARS) != plen)
					goto inval;
				p = strdup(e + 2);
				if (p == NULL)
					goto err;
				len -= plen + 1;
			}
			len -= 2;	/* brackets */
			if (strspn(spec + 1, IPv6_ADDR_CHARS) != len)
				goto inval;
			h = malloc(len + 1);
			if (h == NULL)
				goto err;
			memcpy(h, spec + 1, len);
			h[len] = '\0';
		} else {
			/* "a:d:d:r" */
			if (strspn(spec, IPv6_ADDR_CHARS) != len)
				goto inval;
			h = strdup(spec);
			if (h == NULL)
				goto err;
		}
		rv = SASPEC_RES_IPV6;
		goto success;
	}

	/* Now it's domain name or IPv4 address */
	/* detect port first */
	e = strchr(spec, ':');
	if (e != NULL) {
		if (strchr(e + 1, ':') != NULL)
			goto inval;
		plen = strlen(e + 1);
		if (strspn(e + 1, PORT_CHARS) != plen)
			goto inval;
		p = strdup(e + 1);
		if (p == NULL)
			goto err;
		len -= plen + 1;
		if (e == spec) {
			/* just port provided */
			rv = SASPEC_RES_PORT;
			if (flags & SASPEC_HOST_REQUIRED)
				goto inval;
			else
				goto success;
		}
	}

	/* now check if we see IPv4 address: a.b.c.d */
	if (len < 7 || strspn(spec, IPv4_ADDR_CHARS) != len)
		goto parsedomain;
	s = spec;
	if (s[0] == '.')
		goto inval;
	
	
	for (s = spec, i = 0; s[0] != '\0'; s = e + 1) {
		e = strchr(s, '.');
		if (e == NULL) {
			if (i == 3)
				break;
			else
				goto parsedomain;
		}
		if (e - s == 0)
			goto inval;
		if (++i > 3)
			goto parsedomain;
	}
	
	h = malloc(len + 1);
	if (h == NULL)
		goto err;
	memcpy(h, spec, len);
	h[len] = '\0';
	rv = SASPEC_RES_IPV4;
	goto success;

parsedomain:
	if (flags & SASPEC_HOST_NO_DN)
		goto inval;
	for (e = spec; e != NULL; e = strchr(e, '.'))
		if (e[1] == '.')
			/* double dot */
			goto inval;
	h = malloc(len + 1);
	if (h == NULL)
		goto err;
	memcpy(h, spec, len);
	h[len] = '\0';
	rv = SASPEC_RES_DN;

success:
	*host = h;
	*port = p;
	return rv;

inval:
	errno = EINVAL;
err:
	oerrno = errno;
	free(h);
	free(p);
	errno = oerrno;
	return SASPEC_RES_ERROR;
}

/*
 * Initialize sockaddr structure based on output of split_addr_spec().
 * If the protocol family to be used is AF_UNIX then host shoud contain
 * socket path and port should be NULL.
 *
 * The caller is responsible for providing sockaddr structure of size big
 * enough. The easiest way is to use struct sockaddr_storage from <sys/socket.h>
 * header.
 *
 * sasres argument is the same value as split_addr_spec() returns.
 *
 * Return 0 on success or -1 (and set errno) on error.
 */
int
build_sa(struct bindaddr *ba, const char *host, const char *port,
    int sasres, int for_bind) {
	struct	addrinfo *ai, hint;
	struct	sockaddr_un *sun;

	DPRINTFX(3, "called build_sa(..., \"%s\", \"%s\", %d, %d)", host, port,
	    sasres, for_bind);

	bzero(&hint, sizeof(struct addrinfo));
	hint.ai_flags = AI_NUMERICSERV;
	if (for_bind)
		hint.ai_flags |= AI_PASSIVE;
	hint.ai_socktype = SOCK_STREAM;
	switch (sasres) {
	case SASPEC_RES_IPV4:
		if (host == NULL && !for_bind)
			goto inval;
		hint.ai_family = AF_INET;
		hint.ai_flags |= AI_NUMERICHOST;
		break;

	case SASPEC_RES_IPV6:
		if (host == NULL && !for_bind)
			goto inval;
		hint.ai_family = AF_INET6;
		hint.ai_flags |= AI_NUMERICHOST;
		break;

	case SASPEC_RES_LOCAL:
		if (host == NULL || port != NULL)
			goto inval;
		hint.ai_family = AF_UNIX;
		break;

	case SASPEC_RES_PORT:
		if (port == NULL)
			goto inval;
		hint.ai_family = AF_UNSPEC;
		break;

	case SASPEC_RES_DN:
		if (host == NULL && !for_bind)
			goto inval;
		hint.ai_family = AF_UNSPEC;
		break;

	default:
		goto inval;
	}

	if (sasres == SASPEC_RES_LOCAL) {
		sun = (struct sockaddr_un *)&ba->ss;
		if (strlen(host) >= sizeof(sun->sun_path)) {
			errno = ENAMETOOLONG;
			goto err;
		}
#ifndef __linux__
		sun->sun_len = sizeof(struct sockaddr_un);
#endif
		sun->sun_family = hint.ai_family;
		strncpy(sun->sun_path, host, sizeof(sun->sun_path));
	} else {
		if (getaddrinfo(host, port, &hint, &ai))
			/* XXX: should check error cases and act accordingly */
			goto inval;
		/* Just get the first address. Sorry. */
		ba->slen = ai->ai_addrlen;
		memcpy(&ba->ss, ai->ai_addr, ai->ai_addrlen);
		freeaddrinfo(ai);
	}
	return 0;

inval:
	errno = EINVAL;
err:
	return -1;
}

/*
 * Return -1 (and set errno) on error.
 */
int
fill_sa_from_user(const char *spec, struct bindaddr *ba, int flags,
   const char *defport) {
	int	 rv;
	char	*h, *p;

	p = h = NULL;
	rv = split_addr_spec(spec, &h, &p, flags);
	if (rv == SASPEC_RES_ERROR)
		return -1;
	rv = build_sa(ba, h, (p ? p : defport), rv,
	    (flags & SASPEC_FOR_BINDING));
	free(h);
	free(p);
	return rv;
}

/*
 * Search DNS for new collector node addresses.
 * 
 * On error return NULL and set errno.
 *
 * Uses BIND's resolver(3) library.
 */
struct addrinfo *
find_collectors(const char *peerdomain, int rrtype) {
	struct addrinfo	hint, *hosts = NULL;
	const		char *dn, *prefix;
	char		hostbuf[MAXHOSTNAMELEN];
	/* unsigned	char *ans; */

	if (!initialized) {
		res_init();
		initialized = 1;
	}

	if (rrtype == T_SRV) {
		errno = ENOTSUP;
		return NULL;
	} else if (rrtype != T_A) {
		errno = EINVAL;
		return NULL;
	}

	if (peerdomain == NULL) {
		if (gethostname(hostbuf, MAXHOSTNAMELEN) == -1)
			goto cleanup;
		dn = strchr(hostbuf, '.');
		if (dn == NULL)
			/* Our domain is a top-level one */
			dn = hostbuf;
		else
			dn++;

		prefix = DEF_PEER_DNS_PREFIX;
		if (strlen(dn) + strlen(prefix) + 1 > MAXHOSTNAMELEN) {
			errno = ENAMETOOLONG;
			goto cleanup;
		}
		memmove(hostbuf + strlen(prefix), dn, strlen(dn) + 1);
		memcpy(hostbuf, prefix, strlen(prefix));
	} else if (strlcpy(hostbuf, peerdomain, MAXHOSTNAMELEN)
	    >= MAXHOSTNAMELEN) {
		errno = ENAMETOOLONG;
		return NULL;
	}

	/* XXX: Should lookup SRV records */
	/* XXX: 4096 should be enough until I figure the right way */
	/*
	len = 4096;
	ans = malloc(len);
	if (ans == NULL)
		fatal("find_collectors: malloc");

	if (res_query(dn, C_IN, T_SRV, ans, len) == -1)
		goto cleanup;
	puts(ans);
	*/
	bzero(&hint, sizeof(struct addrinfo));
	hint.ai_flags = AI_NUMERICSERV;
	if (getaddrinfo(hostbuf, PEER_PORT , &hint, &hosts)) {
		errno = ENOENT;    /* XXX: Better errcode translation? */
		hosts = NULL;      /* Play safe */
		goto cleanup;
	}

cleanup:
	/* free(ans); */
	return (hosts);
}
